在 Day2 中,我們見識了 Rust 所有權的特性:一個值只能有一個所有者,賦值就是「過戶」。
但現實中,我們經常需要共享資料(借用),而不是轉移它。
假設你有一本珍貴的書,朋友想借閱幾天。
在 Rust 的世界裡,你不會直接把書「過戶」給朋友(因為這樣你就失去了它),而是「借出」給朋友,並約定好歸還的時間和條件。
這就是 借用(Borrowing) 的主要概念。
Rust 的借用系統建立在看似簡單,但極很強大的規則之上:
要嘛很多人一起讀(不可變借用
&T
),要嘛只有一個人能寫(可變借用&mut T
)
這條規則直接消滅了無數的 data race,讓我們先看看它如何在實際程式碼中運作。
let s1 = String::from("hello");
let s2 = &s1; // s2 借用 s1 的資料
let s3 = &s1; // s3 也借用 s1 的資料
let s4 = &s1; // s4 同樣借用 s1 的資料
println!("s1: {}", s1); // ✅ s1 仍然有效
println!("s2: {}", s2); // ✅ s2 可以讀取
println!("s3: {}", s3); // ✅ s3 可以讀取
println!("s4: {}", s4); // ✅ s4 可以讀取
在這個例子中,s1
仍然是資料的所有者,而 s2
、s3
、s4
都是借用者。
關鍵在於:所有借用都是不可變的,它們只能讀取資料,不能修改。
這就像多個人同時閱讀同一本書,只要沒有人塗改,就不會有衝突。
當你需要修改資料時,情況就完全不同了:
let mut s1 = String::from("hello");
let s2 = &mut s1; // s2 獲得可變借用
// s2.push_str(" world"); // ✅ 可以修改
// println!("s1: {}", s1); // ❌ 編譯錯誤!s1 被借用了
println!("s2: {}", s2); // ✅ s2 可以讀取和修改
注意這裡的關鍵點:
s2
獲得可變借用時,s1
就不能再被使用了mut
才能獲得可變借用這就像圖書館的「獨占閱覽室」,一次只能有一個人使用,而且這個人可以修改書的內容。
讓我們用一個流程圖來理解借用規則:
在 Python 或 JavaScript 中,共享是隱式的,也是危險的:
# Python
data = [1, 2, 3]
reference1 = data
reference2 = data
reference1.append(4)
print(data) # [1, 2, 3, 4] - 意外修改!
print(reference2) # [1, 2, 3, 4] - 也被影響了!
這裡的問題是:你不知道有多少個引用指向同一份資料,也不知道誰會在什麼時候修改它。
Golang 透過指標讓共享變得顯式:
// Go
data := []int{1, 2, 3}
ptr1 := &data
ptr2 := &data
(*ptr1)[0] = 999
fmt.Println(data) // [999, 2, 3] - 被修改了
fmt.Println(*ptr2) // [999, 2, 3] - 也被影響了
雖然共享變得顯式,但 Go 無法在編譯期阻止 data race:
// Go - 這會導致 data race,但編譯器不會阻止
go func() {
(*ptr1)[0] = 100
}()
go func() {
(*ptr2)[1] = 200
}()
Rust 的借用系統結合了顯式性和安全性:
let mut data = vec![1, 2, 3];
let ptr1 = &mut data;
// let ptr2 = &mut data; // ❌ 編譯錯誤!不能同時有兩個可變借用
ptr1[0] = 999;
println!("{:?}", data); // [999, 2, 3]
更重要的是,Rust 在編譯期就阻止了 data race:
// 這在 Rust 中根本無法編譯
let mut data = vec![1, 2, 3];
let ptr1 = &mut data;
let ptr2 = &mut data; // ❌ 編譯錯誤!
// 即使使用多執行緒,編譯器也會阻止
std::thread::spawn(|| {
ptr1[0] = 100; // ❌ 編譯錯誤!ptr1 的生命週期不夠長
});
借用不僅有所有權的概念,還有時間的概念。
let r; // 宣告一個引用變數
{
let x = 5;
r = &x; // ❌ 編譯錯誤!x 的生命週期太短
}
println!("r: {}", r); // r 試圖使用已經被銷毀的 x
這個例子展示了 Rust 的生命週期檢查:
x
在內層作用域中創建r
試圖借用 x
x
在內層作用域結束時就被銷毀了r
在外層作用域中還試圖使用它Rust 編譯器會拒絕這樣的程式碼,因為它違反了「借用不能比被借用的資料活得更久」的基本原則。
讓我們看看借用如何在函式參數中發揮作用:
fn calculate_length(s: &String) -> usize {
s.len() // 借用 s,不取得所有權
}
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // 傳遞借用
println!("The length of '{}' is {}.", s1, len); // s1 仍然有效
}
對比一下如果使用所有權轉移:
fn calculate_length(s: String) -> usize {
s.len() // 取得所有權,s 在函式結束時被銷毀
}
fn main() {
let s1 = String::from("hello");
let len = calculate_length(s1); // s1 的所有權被轉移
// println!("s1: {}", s1); // ❌ 編譯錯誤!s1 已經無效
}
你可能會問:為什麼 Rust 要設計這些看似嚴格的借用規則?
答案是:這些規則直接對應到現實中的併發安全問題。
// 這在 Rust 中無法編譯
let mut data = 0;
let ptr1 = &mut data;
let ptr2 = &mut data; // ❌ 編譯錯誤!
// 即使使用多執行緒也無法編譯
std::thread::spawn(|| {
*ptr1 += 1; // ❌ 編譯錯誤!
});
std::thread::spawn(|| {
*ptr2 += 1; // ❌ 編譯錯誤!
});
let r;
{
let x = 5;
r = &x; // ❌ 編譯錯誤!x 的生命週期不夠長
}
// r 在這裡試圖使用已經被銷毀的 x
借用規則強迫你明確表達意圖:
&T
:我只想讀取,不會修改&mut T
:我需要修改,給我獨占權T
:我要取得所有權,用完就銷毀切片是 Rust 中一個優雅的借用應用:
let s = String::from("hello world");
let hello = &s[0..5]; // 借用字串的一部分
let world = &s[6..11]; // 借用字串的另一部分
println!("{}", hello); // "hello"
println!("{}", world); // "world"
println!("{}", s); // "hello world" - 原始字串仍然有效
切片讓我們可以安全地引用資料的一部分,而不需要複製或轉移所有權。
Rust 的借用系統體現了一種深刻的哲學:
共享是可能的,但必須有明確的契約。
這個契約由編譯器在編譯期強制執行,確保:
對習慣了「隱式共享」的開發者來說,這可能看起來過於嚴格。
但好處是這嚴格性,能夠編寫出既安全又高效的併發程式碼。
在下一天中來探討生命週期 (Lifetimes),看看 Rust 如何確保「借用的時間安全性」。